1. 异步组件:让页面加载更灵活

1.1 异步组件的意义

异步组件的核心在于“异步”二字,即以异步方式加载和渲染组件。为什么要这么做?设想一个大型应用,包含数百个组件,如果所有组件都在页面初始化时同步加载,首屏加载时间会显著增加,用户体验变差。异步组件通过按需加载(lazy loading)解决了这个问题,尤其在以下场景中大放异彩:

  • 代码分割:将应用拆分为多个小块,仅在需要时加载,减少初始加载的资源体积。
  • 服务端组件下发:从服务器动态获取组件,适用于内容管理系统等场景。
  • 优化首屏加载:只加载当前页面必需的组件,提升加载速度。

从原理上看,异步组件的实现并不依赖框架支持,开发者完全可以通过 JavaScript 的动态 import() 手动实现。例如:

// 同步加载
import App from './App.vue'
createApp(App).mount('#app')

// 异步加载
const loader = () => import('./App.vue')
loader().then(App => {
  createApp(App).mount('#app')
})

上述代码展示了如何通过动态 import() 实现异步加载。然而,手动实现异步组件需要处理许多细节,比如加载失败、超时、占位内容等。Vue.js 3 提供了 defineAsyncComponent 函数,封装了这些复杂逻辑,让开发者更专注于业务开发。

1.2 异步组件要解决的问题

一个完善的异步组件需要考虑以下问题:

  1. 加载失败:如果组件加载失败,是否渲染一个错误提示组件?
  2. 加载中状态:组件加载时,是否展示占位内容(如 Loading 组件)?
  3. 加载速度差异:加载可能很快,也可能很慢,是否需要延迟展示 Loading 组件,避免快速加载时的闪烁?
  4. 重试机制:加载失败后,是否允许自动或手动重试?

Vue.js 3 的异步组件通过以下功能应对这些问题:

  • 指定加载失败时渲染的错误组件(errorComponent)。
  • 配置加载中的占位组件(loadingComponent)及其延迟时间(delay)。
  • 设置加载超时时长(timeout)。
  • 提供加载失败后的重试机制。

这些功能的实现离不开 defineAsyncComponent 函数,接下来我们将深入探讨其实现原理。

1.3 异步组件的实现原理

1.3.1 基础实现:封装defineAsyncComponent

defineAsyncComponent 是一个高阶组件,接收一个加载器函数或配置对象,返回一个包装组件。这个包装组件负责根据加载状态渲染不同的内容。以下是其核心实现:

function defineAsyncComponent(loader) {
  let InnerComp = null
  return {
    name: 'AsyncComponentWrapper',
    setup() {
      const loaded = ref(false)
      loader().then(c => {
        InnerComp = c
        loaded.value = true
      })
      return () => {
        return loaded.value ? { type: InnerComp } : { type: Text, children: '' }
      }
    }
  }
}

关键点

  • 高阶组件defineAsyncComponent 返回一个包装组件,负责管理加载状态。
  • 加载状态管理:通过 ref 创建 loaded 标志,跟踪组件是否加载成功。
  • 占位内容:加载未完成时,渲染一个空文本节点作为占位,减少 DOM 操作开销。

使用方式非常简单:

import { defineAsyncComponent } from 'vue'

export default {
  components: {
    AsyncComp: defineAsyncComponent(() => import('./CompA.vue'))
  }
}

在模板中,<AsyncComp /> 就像普通组件一样使用,但背后却是异步加载。

1.3.2 处理超时与错误组件

网络请求的不确定性要求异步组件支持超时和错误处理。defineAsyncComponent 允许通过配置对象指定超时时长和错误组件。例如:

const AsyncComp = defineAsyncComponent({
  loader: () => import('./CompA.vue'),
  timeout: 2000,
  errorComponent: MyErrorComp
})

实现中增加了超时逻辑和错误捕获:

function defineAsyncComponent(options) {
  if (typeof options === 'function') {
    options = { loader: options }
  }
  const { loader, timeout, errorComponent } = options
  let InnerComp = null

  return {
    name: 'AsyncComponentWrapper',
    setup() {
      const loaded = ref(false)
      const error = shallowRef(null)
      const timeoutFlag = ref(false)

      loader()
        .then(c => {
          InnerComp = c
          loaded.value = true
        })
        .catch(err => {
          error.value = err
        })

      let timer = null
      if (timeout) {
        timer = setTimeout(() => {
          timeoutFlag.value = true
          error.value = new Error(`Async component timed out after ${timeout}ms.`)
        }, timeout)
      }
      onUnmounted(() => clearTimeout(timer))

      const placeholder = { type: Text, children: '' }
      return () => {
        if (loaded.value) {
          return { type: InnerComp }
        } else if (error.value && errorComponent) {
          return { type: errorComponent, props: { error: error.value } }
        }
        return placeholder
      }
    }
  }
}

关键点

  • 错误捕获:通过 catch 捕获加载过程中的所有错误(网络失败、超时等),存储在 error 中。
  • 超时处理:设置定时器,超时后创建错误对象并触发错误组件渲染。
  • 错误组件 props:将错误对象作为 props 传递给 errorComponent,便于开发者自定义错误处理逻辑。
  • 清理定时器:组件卸载时通过 onUnmounted 清除定时器,避免内存泄漏。

1.3.3 优化体验:Loading 组件与延迟

网络状况不同,组件加载速度可能差异很大。如果加载很快,立即展示 Loading 组件可能导致闪烁,影响用户体验。为此,Vue.js 3 提供了 delayloadingComponent 选项:

const AsyncComp = defineAsyncComponent({
  loader: () => import('./CompA.vue'),
  delay: 200,
  loadingComponent: {
    setup() {
      return () => ({ type: 'h2', children: 'Loading...' })
    }
  }
})

实现中新增了加载状态管理:

function defineAsyncComponent(options) {
  if (typeof options === 'function') {
    options = { loader: options }
  }
  const { loader, delay, loadingComponent, timeout, errorComponent } = options
  let InnerComp = null

  return {
    name: 'AsyncComponentWrapper',
    setup() {
      const loaded = ref(false)
      const error = shallowRef(null)
      const loading = ref(false)

      let loadingTimer = null
      if (delay) {
        loadingTimer = setTimeout(() => {
          loading.value = true
        }, delay)
      } else {
        loading.value = true
      }

      loader()
        .then(c => {
          InnerComp = c
          loaded.value = true
        })
        .catch(err => {
          error.value = err
        })
        .finally(() => {
          loading.value = false
          clearTimeout(loadingTimer)
        })

      let timer = null
      if (timeout) {
        timer = setTimeout(() => {
          error.value = new Error(`Async component timed out after ${timeout}ms.`)
        }, timeout)
      }
      onUnmounted(() => {
        clearTimeout(timer)
        clearTimeout(loadingTimer)
      })

      const placeholder = { type: Text, children: '' }
      return () => {
        if (loaded.value) {
          return { type: InnerComp }
        } else if (error.value && errorComponent) {
          return { type: errorComponent, props: { error: error.value } }
        } else if (loading.value && loadingComponent) {
          return { type: loadingComponent }
        }
        return placeholder
      }
    }
  }
}

关键点

  • 加载状态:通过 loading 标志跟踪加载中状态。
  • 延迟展示:通过 delay 设置定时器,仅当加载时间超过指定值时才展示 loadingComponent
  • 清理定时器:无论加载成功或失败,都在 finally 中清理 loadingTimer,避免 Loading 组件滞留。
  • 卸载逻辑:修改 unmount 函数,确保 Loading 组件能正确卸载,递归处理组件的 subTree

1.3.4 重试机制

网络不稳定时,组件加载失败是常见问题。Vue.js 3 支持通过 onError 回调实现重试机制。以下是实现思路:

function defineAsyncComponent(options) {
  if (typeof options === 'function') {
    options = { loader: options }
  }
  const { loader, onError } = options
  let InnerComp = null
  let retries = 0

  function load() {
    return loader()
      .catch(err => {
        if (onError) {
          return new Promise((resolve, reject) => {
            const retry = () => {
              resolve(load())
              retries++
            }
            const fail = () => reject(err)
            onError(retry, fail, retries)
          })
        } else {
          throw err
        }
      })
  }

  return {
    name: 'AsyncComponentWrapper',
    setup() {
      const loaded = ref(false)
      const error = shallowRef(null)
      const loading = ref(false)

      load()
        .then(c => {
          InnerComp = c
          loaded.value = true
        })
        .catch(err => {
          error.value = err
        })
        .finally(() => {
          loading.value = false
        })

      return () => {
        if (loaded.value) {
          return { type: InnerComp }
        } else if (error.value && options.errorComponent) {
          return { type: options.errorComponent, props: { error: error.value } }
        } else if (loading.value && options.loadingComponent) {
          return { type: options.loadingComponent }
        }
        return { type: Text, children: '' }
      }
    }
  }
}

关键点

  • 重试逻辑:通过 load 函数封装加载逻辑,捕获错误后调用 onError,将 retryfail 函数交给用户。
  • 重试计数:通过 retries 记录重试次数,供用户决定是否继续重试。
  • 用户控制:用户通过 onError(retry, fail, retries) 决定是重试还是抛出错误。

使用示例:

defineAsyncComponent({
  loader: () => import('./CompA.vue'),
  onError(retry, fail, retries) {
    if (retries < 3) {
      retry()
    } else {
      fail()
    }
  }
})

2. 函数式组件:简单至上的选择

2.1 函数式组件的核心

函数式组件是一个普通函数,返回虚拟 DOM,特点是无状态、无生命周期,编写简单直观。在 Vue.js 2 中,函数式组件因性能优势被广泛使用,而在 Vue.js 3 中,由于有状态组件的初始化性能已大幅优化,函数式组件的主要优势在于其简单性

定义一个函数式组件非常简单:

function MyFuncComp(props) {
  return { type: 'h1', children: props.title }
}
MyFuncComp.props = {
  title: String
}

关键点

  • 无状态:函数式组件不维护自身状态,仅依赖传入的 props
  • 静态 props:通过在函数上定义 props 属性指定接收的参数类型。

2.2 函数式组件的实现原理

Vue.js 3 通过复用有状态组件的逻辑实现函数式组件。在 patch 函数中,通过检测 vnode.type 的类型区分组件类型:

function patch(n1, n2, container, anchor) {
  const { type } = n2
  if (typeof type === 'string') {
    // 处理元素
  } else if (typeof type === 'object' || typeof type === 'function') {
    // 处理组件(对象为有状态组件,函数为函数式组件)
    if (!n1) {
      mountComponent(n2, container, anchor)
    } else {
      patchComponent(n1, n2, anchor)
    }
  }
}

mountComponent 函数中,针对函数式组件进行特殊处理:

function mountComponent(vnode, container, anchor) {
  const isFunctional = typeof vnode.type === 'function'
  let componentOptions = vnode.type

  if (isFunctional) {
    componentOptions = {
      render: vnode.type,
      props: vnode.type.props
    }
  }
  // 继续有状态组件的初始化逻辑
}

关键点

  • 类型检测:通过 typeof vnode.type === 'function' 判断是否为函数式组件。
  • 统一处理:将函数式组件的 type 作为 render 函数,props 作为选项,复用有状态组件的挂载逻辑。
  • 性能优化:跳过不必要的数据初始化和生命周期钩子,提升初始化效率。

2.3 函数式组件的应用场景

函数式组件适用于以下场景:

  • 简单展示组件:如纯展示的标题、图标等,无需状态管理。
  • 性能敏感场景:虽然 Vue.js 3 中性能差距不大,但在复杂列表渲染中,函数式组件仍可减少少量开销。
  • 代码简洁:适合快速开发原型或轻量组件。